跳到主要内容

CSSInJS:快速掌握styled-components

CSS in JS,顾名思义就是用 js 来写 css。

它也是一种很流行的 css 管理方案。

比如 styled-components 的样式是这样写:

可以传参数。

然后用的时候当作组件一样用:

样式用 js 写,可以当成组件用,可以传参,这是 CSS in JS 的方案独有的体验。

接下来我们也体验一下:

npx create-vite styled-components-test

用 vite 创建个项目。

安装 styled-components:

npm install

npm install --save styled-components

去掉 index.css 和 StrictMode:

然后改下 App.tsx:

import { styled } from "styled-components";

const Title = styled.h1`
font-size: 30px;
text-align: center;
color: blue;
`;

const Header = styled.div`
padding: 20px;
background: pink;
`;

function App() {
return (
<Header>
<Title>Hello World!</Title>
</Header>
);
}

export default App;

跑起来看下:

npm run dev

样式生效了:

打开 devtools 看下:

可以看到 styled.div、styled.h1 会创建对应的标签,然后样式会生成一个唯一的 className。

所以说,用 styled-components 不用担心样式冲突的问题。

继续看,styled-components 的 styled.xx 可以作为组件用,那自然是可以传参的:

import { styled } from "styled-components";

const Title = styled.h1<{ color?: string }>`
font-size: 30px;
text-align: center;
color: ${(props) => props.color || "blue"};
`;

const Header = styled.div`
padding: 20px;
background: pink;
`;

function App() {
return (
<Header>
<Title>Hello World!</Title>
<Title color="green">Hello World!</Title>
<Title color="black">Hello World!</Title>
</Header>
);
}

export default App;

我们给 Title 样式组件添加一个 color 参数,然后分别传入 green、black。

看下效果:

确实样式组件用起来和其他 React 组件体验一样,加的 ts 类型也会有提示:

这也是为啥这个库叫 styled-components,样式组件。

有的时候,样式需要基于已有的做扩展,比如我有一个 Button 的样式,另一种 Button 和它大部分一样,但有所不同。

这时候就可以这样写:

import { styled } from "styled-components";

const Button = styled.button<{ color?: string }>`
font-size: 20px;
margin: 5px 10px;
border: 2px solid #000;
color: ${(props) => props.color || "blue"};
`;

const Button2 = styled(Button)`
border-radius: 8px;
`;
function App() {
return (
<div>
<Button color="red">Hello World!</Button>
<Button2 color="red">Hello World!</Button2>
</div>
);
}

export default App;

如果你还想改样式组件的标签,可以用 as:

styled() 除了可以给样式组件扩展样式外,还可以给普通组件加上样式:

import { FC, PropsWithChildren } from "react";
import { styled } from "styled-components";

interface LinkProps extends PropsWithChildren {
href: string;
className?: string;
}

const Link: FC<LinkProps> = (props) => {
const { href, className, children } = props;

return (
<a href={href} className={className}>
{children}
</a>
);
};

const StyledLink = styled(Link)`
color: green;
font-size: 40px;
`;

function App() {
return (
<div>
<StyledLink href="#aaa">click me</StyledLink>
</div>
);
}

export default App;

比如我们给 Link 组件加上样式。

这里要注意,Link 组件必须接收 className 参数,因为 styled-components 会把样式放到这个 className 上:

我们知道,样式组件也是可以接受参数的,为了区分两者,我们一般都是样式组件的 props 用 $ 开头:

const StyledLink = styled(Link)<{ $color?: string }>`
color: ${(props) => props.$color || "green"};
font-size: 40px;
`;

function App() {
return (
<div>
<StyledLink href="#aaa" $color="purple">
click me
</StyledLink>
</div>
);
}

默认情况下,样式组件会透传所有不是它的 props 给被包装组件:

样式组件包了一层,自然是可以修改 props 的:

用 attrs 方法,接收传入的 props 返回修改后的 props。

import { FC, PropsWithChildren } from "react";
import { styled } from "styled-components";

interface LinkProps extends PropsWithChildren {
href: string;
className?: string;
}

const Link: FC<LinkProps> = (props) => {
console.log(props);

const { href, className, children } = props;

return (
<a href={href} className={className}>
{children}
</a>
);
};

const StyledLink = styled(Link).attrs<{ $color?: string }>((props) => {
console.log(props);

props.$color = "orange";
props.children = props.children + " 光";
return props;
})`
color: ${(props) => props.$color || "green"};
font-size: 40px;
`;

function App() {
return (
<div>
<StyledLink href="#aaa" $color="purple">
click me
</StyledLink>
</div>
);
}

export default App;

attrs 支持对象和函数,简单的场景直接传对象也可以:

const Input = styled.input.attrs({ type: "checkbox" })`
width: 30px;
height: 30px;
`;

那伪类选择器、伪元素选择器这些呢?

当然也是支持的。

import { styled } from "styled-components";

const ColoredText = styled.div`
color: blue;

&:hover {
color: red;
}

&::before {
content: "* ";
}
`;

function App() {
return (
<>
<ColoredText>Hello styled components</ColoredText>
</>
);
}

export default App;

写法和之前一样。

但 styled components 这个 & 和 scss 里的 & 含义还不大一样。

它指的是同一个样式组件的实例,这里也就是 ColoredText 的实例。

所以可以这样写:

import { styled } from "styled-components";

const ColoredText = styled.div`
color: blue;

&:hover {
color: red;
}

&::before {
content: "* ";
}

&.aaa + & {
background: lightblue;
}

&.bbb ~ & {
background: pink;
}
`;

function App() {
return (
<>
<ColoredText>Hello styled components</ColoredText>
<ColoredText className="aaa">Hello styled components</ColoredText>
<ColoredText>Hello styled components</ColoredText>
<ColoredText className="bbb">Hello styled components</ColoredText>
<div>Hello styled components</div>
<ColoredText>Hello styled components</ColoredText>
<ColoredText>Hello styled components</ColoredText>
</>
);
}

export default App;

这里 &.aaa + & 就是 .aaa 的 ColoredText 样式组件之后的一个 ColoredText 样式组件实例。

&.bbb ~ & 就是 .bbb 的 ColoredText 样式组件之后的所有 ColoredText 样式组件实例。

此外,如果你把 & 全换成 &&,你会发现效果也一样:

那什么时候用 &、什么时候用 && 呢?

当你和全局样式冲突的时候。

styled-components 用 createGlobalStyle 创建全局样式:

我们全局指定 ColoredText 的 color 为 green,然后组件里指定 color 为 blue。

看下效果:

每个 ColorText 组件都会有一个 src-aYaIB 的 className,全局样式就是给这个 className 加了 color 为 green 的样式。

可以看到,组件里写的 color: blue 被覆盖了。

这时候你这样写是没用的:

用 && 才能覆盖:

它通过 .aaa.aaa 这样的方式实现了样式优先级的提升:

那动画怎么写呢?

有单独的 api:

import { styled, keyframes } from "styled-components";

const rotate = keyframes`
from {
transform: rotate(0deg);
}

to {
transform: rotate(360deg);
}
`;

const Rotate = styled.div`
display: inline-block;
animation: ${rotate} 2s linear infinite;
font-size: 50px;
padding: 30px;
`;

function App() {
return <Rotate>X</Rotate>;
}

export default App;

通过 keyframes 来编写动画,然后在 animation 里引用。

看下效果:

它为 @keyframes 生成了一个唯一 ID:

这大概就是加一个 keyframes 的 api 的意义。

此外,如果你想复用部分 css,要这样写:

const animation = css`
animation: ${rotate} 2s linear infinite;
`;

const Rotate = styled.div`
display: inline-block;
${animation}
font-size: 50px;
padding: 30px;
`;

不加 css 是不会生效的,你可以试一下。

抽出来的 css 也是可以用 props 的:

import { styled, keyframes, css } from "styled-components";

const rotate = keyframes`
from {
transform: rotate(0deg);
}

to {
transform: rotate(360deg);
}
`;

const animation = css<{ $duration: number }>`
animation: ${rotate} ${(props) => props.$duration}s linear infinite;
`;

const Rotate = styled.div<{ $duration: number }>`
display: inline-block;
${animation}
font-size: 50px;
padding: 30px;
`;

function App() {
return <Rotate $duration={3}>X</Rotate>;
}

export default App;

但是 css 声明了类型,用到了这部分样式的 styled.xxx 也需要声明类型。

如果你希望样式组件用的时候可以传入一些样式,那可以用 RuleSet:

import { styled, keyframes, css, RuleSet } from "styled-components";

const rotate = keyframes`
from {
transform: rotate(0deg);
}

to {
transform: rotate(360deg);
}
`;

const animation = css<{ $duration: number }>`
animation: ${rotate} ${(props) => props.$duration}s linear infinite;
`;

const Rotate = styled.div<{ $duration: number; otherStyles: RuleSet }>`
display: inline-block;
${animation}
font-size: 50px;
padding: 30px;
${(props) => props.otherStyles}
`;

function App() {
return (
<Rotate
$duration={3}
otherStyles={[
{ border: "1px", background: "pink" },
{ boxShadow: "0 0 3px blue" },
]}>
X
</Rotate>
);
}

export default App;

它是一个样式对象的数组类型:

可以用的时候传入一些样式:

最后,styled-components 还有 theme 的 api。

这个也很简单,你会用 react 的 context 就会用这个:

import { styled, ThemeProvider } from "styled-components";

const Aaa = styled.div`
width: 100px;
height: 100px;
background: ${(props) => (props.theme.dark ? "black" : "#ccc")};
`;
function Content() {
return <Aaa></Aaa>;
}

function App() {
return (
<ThemeProvider theme={{ dark: true }}>
<Content></Content>
</ThemeProvider>
);
}

export default App;

每个样式组件都有 props.theme 可以读取当前 theme 对象,然后这个对象可以通过 useTheme 读取,通过 ThemeProvider 修改。

import { useState } from "react";
import { styled, ThemeProvider, useTheme } from "styled-components";

const Aaa = styled.div`
width: 100px;
height: 100px;
background: ${(props) => (props.theme.dark ? "black" : "#ccc")};
`;
function Content() {
const theme = useTheme();
const [dark, setDark] = useState < boolean > theme.dark;

return (
<>
<button onClick={() => setDark(!dark)}>切换</button>
<ThemeProvider theme={{ dark }}>
<Aaa></Aaa>
</ThemeProvider>
</>
);
}

function App() {
return (
<ThemeProvider theme={{ dark: true }}>
<Content></Content>
</ThemeProvider>
);
}

export default App;

我们用 useTheme 读取了当前 theme,然后点击按钮的时候 setState 触发重新渲染,通过 ThemeProvider 修改了 theme 的值。

这就是 styled-components 的 theme 功能。

上面的过一遍,styled-components 就算掌握的差不多了

那最后我们来思考下,用 styled-components 有啥优缺点呢?

先来看下好处:

用了 styled-components 之后,你的 className 都是这样的:

没有样式冲突问题,不需要类似 CSS Modules 这种方案。

而且你可以用 js 来写样式逻辑,而且封装方式也是 React 组件的方式,这个是挺爽的。

不然你要学 scss 的函数的语法,比如这样:

@function multiple-box-shadow($n) {
$value: "#{random(2000)}px #{random(2000)}px #FFF";
@for $i from 2 through $n {
$value: "#{$value} , #{random(2000)}px #{random(2000)}px #FFF";
}
@return unquote($value);
}

#stars {
width: 1px;
height: 1px;
box-shadow: multiple-box-shadow(700);
}

scss 的 for 循环、if else 还有函数等的语法都要单独学习。

相比之下,还是 styled-components 直接用 js 来写样式组件的逻辑更爽。

这就像很多人不喜欢 vue 的 template 写法,更喜欢 React 的 jsx 一样,可以直接用 js 来写逻辑。

当然,styled-components 也有不好的地方,比如:

你的 React 项目里会多出特别多样式组件:

随便找一个组件,一眼望去全是样式组件。

你的 React DevTools 里也是一堆 styled-components 的组件:

当然,这些也不是啥大问题,styled-components 整体还是很好用的。

案例代码上传了小册仓库

总结

CSS in JS 就是用 js 来写 css。

今天我们学习了最流行的 CSS in JS 库 styled-components。

它的特点就是样式组件,用 styled.div、styled() 可以创建样式组件。

样式组件可以传参数,可以通过 attrs() 修改参数。

通过 keyframes 来声明动画样式,通过 css 来复用某段样式,通过 createGlobalStyle 创建全局样式。

写样式的时候,通过 & 代表当前样式组件的实例,当样式和全局样式冲突的时候,还可以 && 提高优先级。

styled-components 还支持 theme,可以通过 ThemeProvider 修改 theme 值,通过 useTheme 来读取,每个样式组件里都可以通过 props.theme 拿到当前 theme,然后展示不同样式。

styled-components 相比 scss 等方案有好有坏:

  • 没有 className 冲突问题,不需要 CSS Modules
  • 用 js 来写逻辑,不需要学习单独的 scss 语法
  • 项目里会多很多的样式组件,和普通组件混在一起
  • React DevTools 里会有很多层的样式组件

总体来说,styled-components 还是很有不错,如果你喜欢通过 React 组件的方式来写样式这种方式,可以考虑使用。

我最近在维护的一个项目,用 styled-components 好多年了,大项目用也没问题。